export default class CanvasScene {

    canDrag = true
    fingers = 1; // 触摸的指头个数
    startFingerDistance = 0; // 双指间的距离
    currTouches = []; // 记录当前(上次)触摸坐标
    touchMoveTimer = null; // 触摸移动计时器
    touchEndTimer = null; // 触摸结束计时器
    imageCache = {} // 图片缓存
    components = [] // 画布上组件集合
    lastZoomState = 0

    /**
     *
     * @param options {{
     *     translate?: { x: number, y: number }
     *     isThrottled?: boolean
     *     throttleInterval?: number
     *     element: UniNamespace.NodeInfo | UniNamespace.NodeCallbackResult,
     *     pixelRatio?: number,
     *     scale?: number,
     *     minScale?: number
     *     maxScale?: number
     *     scaleStep?: number
     *     touchScaleStep?: number
     * }}
     */
    constructor(options = {}) {
        const {element} = options
        if (!element) {
            console.error('no canvas element found');
            return
        }
        this.pixelRatio = options.pixelRatio ?? 1; // 像素比
        this.canvas = element.node;
        this.width = element.width * this.pixelRatio;
        this.height = element.height * this.pixelRatio;
        this.canvas.width = this.width;
        this.canvas.height = this.height;
        this.ctx = this.canvas.getContext('2d');
        this.translate = options.translate ?? {x: 0, y: 0}; // 拖动偏移
        this.isThrottled = options.isThrottled ?? true; // 是否启用节流绘制
        this.throttleInterval = options.throttleInterval ?? Math.round(1 / 24 * 1000); // 节流间隔时间，单位ms
        this.defaultScale = options.scale ?? 1;
        this.scale = options.scale ?? 1;
        this.minScale = options.minScale ?? 0.5;
        this.maxScale = options.maxScale ?? 2;
        this.scaleStep = options.scaleStep ?? 0.1
        this.touchScaleStep = options.touchScaleStep ?? 0.05;
        this.zoomCenter = {x: this.width / 2, y: this.height / 2};
        this.currScale = this.scale;
        // 事件
        this.events = {touchstart: [], touchmove: [], touchend: [], tap: [], 'component:selected': [], 'component:unselected': []};
    }

    /* ===========================
     * 画布操作
     * =========================== */

    draw(callback = () => {}) {
        this.move()
        this.drawComponents()
        callback()
    }

    move() {
        this.canvas.width = this.width
        this.canvas.height = this.height
        this.ctx.translate(this.translate.x, this.translate.y);
        this.ctx.scale(this.scale, this.scale);
        // console.log('当前偏移:', this.translate.x, this.translate.y)
    }

    drag(e) {
        if (!this.canDrag) return
        this.translate.x += (this.getTouchPoint(e, 0).x - this.currTouches[0].x) * this.pixelRatio
        this.translate.y += (this.getTouchPoint(e, 0).y - this.currTouches[0].y) * this.pixelRatio
        // console.log('当前拖拽:', this.translate.x, this.translate.y)
        this.draw()
    }

    zoomIn() {
        this.lastZoomState = 1
        this.zoomTo(this.currScale + this.scaleStep)
    }

    zoomOut() {
        this.lastZoomState = -1
        this.zoomTo(this.currScale - this.scaleStep)
    }

    /**
     * 缩放处理
     * @param scale 比例
     * @param zCenter 缩放中心
     */
    zoomTo(scale, zCenter = undefined) {
        // 限制缩放
        this.scale = scale
        this.scale = this.scale > this.maxScale ? this.maxScale : this.scale
        this.scale = this.scale < this.minScale ? this.minScale : this.scale
        //
        const zoomCenter = zCenter || this.zoomCenter
        if (this.lastZoomState > 0) {
            // 放大
            const scaleRate = this.scale / this.currScale
            this.translate.x = zoomCenter.x - zoomCenter.x * scaleRate + this.translate.x * scaleRate
            this.translate.y = zoomCenter.y - zoomCenter.y * scaleRate + this.translate.y * scaleRate
        } else if (this.lastZoomState < 0) {
            const scaleRate = this.currScale / this.scale
            this.translate.x = zoomCenter.x - zoomCenter.x / scaleRate + this.translate.x / scaleRate
            this.translate.y = zoomCenter.y - zoomCenter.y / scaleRate + this.translate.y / scaleRate
        }
        this.draw()
        this.currScale = this.scale
    }

    clear() {
        this.canvas.width = this.width
        this.canvas.height = this.height
        // 复位
        this.translate = {x: 0, y: 0}
        this.scale = this.defaultScale
        this.currScale = this.defaultScale
        this.ctx.translate(this.translate.x, this.translate.y)
        this.ctx.scale(this.scale, this.scale)
        this.ctx.clearRect(0, 0, this.width, this.height)
        this.components.length = 0
    }

    drawShape(opt, isAddComponent = true) {
        if (opt.points.length < 3) return
        const obj = {type: 'Shape', angle: opt.angle, points: opt.points}
        this.rotate(obj, () => {
            this.ctx.beginPath()
            this.ctx.lineWidth = opt.style.lineWidth
            this.ctx.fillStyle = opt.style.fillStyle
            this.ctx.strokeStyle = opt.style.strokeStyle ?? '#000'
            // 虚线
            if (opt.style.strokeStyle && opt.style.lineDash && opt.style.lineDash.length > 0) {
                this.ctx.setLineDash(opt.style.lineDash)
            }
            // 绘制多边形
            for (let i = 0; i < opt.points.length; i++) {
                const point = opt.points[i]
                if (i === 0) {
                    this.ctx.moveTo(point.x, point.y)
                } else {
                    this.ctx.lineTo(point.x, point.y)
                }
            }
            this.ctx.closePath()
            if (opt.style.strokeStyle) {
                this.ctx.stroke()
                this.ctx.setLineDash([])
            }
            if (opt.style.fillStyle) {
                this.ctx.fill()
            }
        })
        if (isAddComponent) {
            this.addComponent('Shape', opt)
        }
    }

    /**
     *
     * @param opt {{
     *     id?: string
     *     angle: number
     *     points: Array<{ x: number, y: number }>,
     *     style: { image: string, width: number, height: number }
     * }}
     * @param isAddComponent { boolean }
     */
    drawImage(opt, isAddComponent = true) {
        const point = opt.points[0]
        if (!point) return
        const obj = {type: 'Image', angle: opt.angle, points: opt.points}
        let image = this.imageCache[opt.style.image]
        if (!image) {
            image = this.canvas.createImage()
            image.src = opt.style.image
            image.onload = () => {
                this.imageCache[opt.style.image] = image
                this.rotate(obj, () => {
                    this.ctx.clearRect(point.x, point.y, opt.style.width, opt.style.height)
                    this.ctx.drawImage(image, point.x, point.y, opt.style.width, opt.style.height)
                })
            }
        } else {
            this.rotate(obj, () => {
                this.ctx.clearRect(point.x, point.y, opt.style.width, opt.style.height)
                this.ctx.drawImage(image, point.x, point.y, opt.style.width, opt.style.height)
            })
        }
        if (isAddComponent) {
            this.addComponent('Image', opt)
        }
    }

    /**
     * 旋转绘制
     * @param obj {{ type: string, angle: number, points: Array<{x: number, y:number}>}}
     * @param callback {() => void}
     */
    rotate(obj, callback) {
        const angle = obj.angle ?? 0
        const centerPoint = this.getRotateCenterPoint(obj)
        this.ctx.save()
        this.ctx.translate(centerPoint.x, centerPoint.y)
        this.ctx.rotate((angle * Math.PI) / -180)
        this.ctx.translate(-centerPoint.x, -centerPoint.y)
        callback()
        this.ctx.restore()
    }

    /**
     * 获取旋转中心点
     * @param obj {{type: string, points: Array<{x: number, y: number}>}}
     * @return {{x: number, y: number}}
     */
    getRotateCenterPoint(obj) {
        switch (obj.type) {
            case 'Point':
                return obj.points[0]
            default:
                return this.getPolygonCenterPoint(obj.points)
        }
    }

    drawComponents() {
        this.components.sort((a, b) => a.zIndex - b.zIndex)
        this.components.forEach(component => {
            this[`draw${component.type}`](component)
        })
    }

    /* ===========================
     * 画布中的组件函数
     * =========================== */

    addComponent(type, opt) {
        const boundingRect = this.getBoundingBox(opt.points)
        const comp = {
            id: opt.id ?? `COMP-${+new Date()}`,
            type: type,
            angle: opt.angle ?? 0,
            zIndex: opt.zIndex ?? 0,
            points: JSON.parse(JSON.stringify(opt.points)),
            style: opt.style ?? {},
            text: opt.text,
            boundingRect: boundingRect,
            selected: opt.selected ?? false
        }
        // 获取存在的对象
        const existComp = this.getComponent(comp.id)
        if (existComp) {
            existComp.type = type
            existComp.angle = comp.angle
            existComp.zIndex = comp.zIndex
            existComp.points = comp.points
            existComp.style = comp.style
            existComp.text = comp.text
            existComp.boundingRect = comp.boundingRect
            existComp.selected = comp.selected
        } else {
            this.components.push(comp)
        }
        return comp;
    }

    getComponent(id) {
        return this.components.find(item => item.id === id)
    }

    removeComponent(id) {
        const index = this.components.findIndex(item => item.id === id)
        if (index !== -1) {
            this.components.splice(index, 1)
        }
    }

    /**
     * 获取当前选中的组件
     * @return {*}
     */
    getSelectedComponent() {
        return this.components.find(component => component.selected)
    }

    /**
     * 清除所有组件的选中状态
     */
    clearSelectedComponent() {
        this.components.forEach(component => {
            component.selected = false
            component.zIndex = 0
        })
    }

    /* ===========================
     * 事件
     * =========================== */

    on(type, callback) {
        if (!this.events[type]) return;
        this.events[type].push(callback);
    }

    off(type, callback) {
        if (!this.events[type]) return;
        const index = this.events[type].indexOf(callback)
        if (index !== -1) {
            this.events[type].splice(index, 1)
        }
    }

    triggerEvent(type, params) {
        this.events[type].forEach(callback => {
            callback(params);
        });
    }

    resetEvents() {
        Object.keys(this.events).forEach(key => {
            this.events[key] = []
        })
    }

    onTouchStart(e) {
        // 获取触摸指个数
        this.fingers = e.touches.length
        if (this.fingers > 2) return
        // 记录当前
        this.currTouches = JSON.parse(JSON.stringify(e.touches))
        if (this.fingers === 1) {
            // 单指
            this.triggerEvent("component:unselected", this.getSelectedComponent())
            this.clearSelectedComponent()
            this.triggerEvent("touchstart", {point: this.getPoint(e), event: e})
        } else if (this.fingers === 2) {
            // 双指
            this.startFingerDistance = this.get2PointDistance(e)
        }
    }

    onTouchMove(e) {
        if (this.fingers > 2 || this.isEqualTouchPoint(this.currTouches[0], e.changedTouches[0])) return
        if (this.isThrottled) {
            if (this.touchMoveTimer) return
            this.touchMoveTimer = setTimeout(() => this.execTouchMove(e), this.throttleInterval)
        } else {
            this.execTouchMove(e)
        }
    }

    onTouchEnd(e) {
        if (this.isThrottled) {
            this.touchEndTimer = setTimeout(() => this.execTouchEnd(e), this.throttleInterval);
        } else {
            this.execTouchEnd(e)
        }
    }

    onTap(e) {
        if (this.fingers !== 1) return
        const point = this.getPoint(e)
        this.triggerEvent("tap", {point, event: e});
        this.clearSelectedComponent()
        this.components.forEach(component => {
            if (this.isInsidePolygon(point, component.points, component.angle)) {
                component.selected = true
                component.zIndex = 1
                this.triggerEvent('component:selected', component)
            }
        })
    }

    execTouchMove(e) {
        if (this.fingers === 1) {
            this.triggerEvent("touchmove", {point: this.getPoint(e), event: e})
            // 拖拽
            this.drag(e)
        } else if (this.fingers === 2 && e.touches.length === 2 && this.currTouches.length === 2) {
            // 判断移动方向为同一方向则拖拽否则缩放
            if (this.isSameDirection(this.currTouches[0], this.getTouchPoint(e, 0), this.currTouches[1], this.getTouchPoint(e, 1))) {
                // 拖拽
                this.drag(e)
            } else {
                // 缩放
                const endFingerDistance = this.get2PointDistance(e)
                if (endFingerDistance === this.startFingerDistance) {
                    this.lastZoomState = 0
                } else {
                    this.lastZoomState = (endFingerDistance - this.startFingerDistance) > 0 ? 1 : -1
                }
                this.startFingerDistance = endFingerDistance
                const first = this.getTouchPoint(e, 0)
                const second = this.getTouchPoint(e, 1)
                const zoomCenter = {
                    x: (first.x + second.x) / 2 * this.pixelRatio,
                    y: (first.y + second.y) / 2 * this.pixelRatio,
                }
                // 执行缩放
                let scaleRate = this.currScale
                if (this.lastZoomState > 0) {
                    scaleRate = scaleRate + this.touchScaleStep
                } else if (this.lastZoomState < 0) {
                    scaleRate = scaleRate - this.touchScaleStep
                }
                this.zoomTo(scaleRate, zoomCenter)
            }
        }
        this.currTouches = e.touches
        clearTimeout(this.touchMoveTimer)
        this.touchMoveTimer = null
    }

    execTouchEnd(e) {
        if (this.fingers === 1) {
            this.onTap(e)
        }
        this.triggerEvent("touchend", {point: this.getPoint(e), event: e})
        clearTimeout(this.touchEndTimer)
        this.touchEndTimer = null
    }

    /* ===========================
     * 辅助函数
     * =========================== */

    /**
     * 是否是相同的坐标
     * @param curr { { x: number, y: number } } 当前
     * @param next { { x: number, y: number } } 下一个点
     * @returns {boolean} true or false
     */
    isEqualTouchPoint(curr, next) {
        return Math.round(next.x) === Math.round(curr.x) && Math.round(next.y) === Math.round(curr.y)
    }

    /**
     * 检测两点移动方向是否是同一方向
     * @param sp1 { Touch & { x: number, y: number } }
     * @param ep1 { Touch & { x: number, y: number } }
     * @param sp2 { Touch & { x: number, y: number } }
     * @param ep2 { Touch & { x: number, y: number } }
     */
    isSameDirection(sp1, ep1, sp2, ep2) {
        // ep1 - sp1 向量
        const vector1 = {
            x: ep1.x - sp1.x,
            y: ep1.y - sp1.y
        }
        // ep2 - sp2 向量
        const vector2 = {
            x: ep2.x - sp2.x,
            y: ep2.y - sp2.y
        }
        if (vector1.x === 0 && vector1.y === 0 && vector2.x === 0 && vector2.y === 0) return true
        if ((vector1.x === 0 && vector1.y === 0) || (vector2.x === 0 && vector2.y === 0)) return false
        return !(
            (vector1.x < 0 && vector2.x > 0) ||
            (vector1.y < 0 && vector2.y > 0) ||
            (vector1.x > 0 && vector2.x < 0) ||
            (vector1.y > 0 && vector2.y < 0)
        )
    }

    /**
     * 计算触摸2点间的距离
     * @param e { TouchEventInit & { touches: { x: number, y: number}[] } }
     */
    get2PointDistance(e) {
        if (e.touches.length !== 2) return 0
        const x = (e.touches[1].x - e.touches[0].x) * this.pixelRatio
        const y = (e.touches[1].y - e.touches[0].y) * this.pixelRatio
        return Math.sqrt(x * x + y * y)
    }

    /**
     * 获取在画布上触摸的坐标
     * @param e 触摸事件
     * @returns {{x: number, y: number}} 返回坐标
     */
    getPoint(e) {
        const p = this.getTouchPoint(e, 0);
        return {
            x: ((p.x * this.pixelRatio) - this.translate.x) / this.scale,
            y: ((p.y * this.pixelRatio) - this.translate.y) / this.scale,
        }
    }

    /**
     * 获取客户端屏幕坐标
     * @param e 触摸事件
     * @returns {{x: number, y: number}} 返回坐标
     */
    getScreenPoint(e) {
        const p = this.getTouchPoint(e, 0);
        return {
            x: p.clientX,
            y: p.clientY,
        }
    }

    /**
     * 获取触摸坐标
     * @param e { TouchEventInit }
     * @param index {number}
     * @returns { * | Touch & { x: number, y: number } }
     */
    getTouchPoint(e, index) {
        if (e.touches && e.touches[index]) return e.touches[index]
        return e.changedTouches && e.changedTouches[index]
    }

    /**
     * 获取多边形中心点
     * @param points {Array<{x: number, y: number}>}
     * @return {{x:number, y:number}}
     */
    getPolygonCenterPoint(points) {
        const center = {x: 0, y: 0};
        points.forEach(point => {
            center.x += point.x
            center.y += point.y
        })
        center.x = center.x / points.length
        center.y = center.y / points.length
        return center;
    }

    /**
     * 获取边界框
     * @param points {Array<{x: number, y: number}>}
     * @return {{tl: {x: number, y: number}, tr: {x: number, y: number}, br: {x: number, y: number}, bl: {x: number, y: number}}}
     */
    getBoundingBox(points) {
        const boundingBox = {}
        // 计算最左、最右、最上和最下的坐标
        const {x, y} = points[0]
        let left = x, right = x
        let top = y, bottom = y
        for (let i = 1; i < points.length; i++) {
            if (points[i].x < left) {
                left = points[i].x
            } else if (points[i].x > right) {
                right = points[i].x
            }
            if (points[i].y < top) {
                top = points[i].y
            } else if (points[i].y > bottom) {
                bottom = points[i].y
            }
        }
        boundingBox.tl = {x: left, y: top}
        boundingBox.tr = {x: right, y: top}
        boundingBox.br = {x: right, y: bottom}
        boundingBox.bl = {x: left, y: bottom}
        return boundingBox
    }

    rotatePoint(point, center, angle = 0) {
        // 弧度
        const radians = (angle * Math.PI) / 180
        const dx = point.x - center.x
        const dy = point.y - center.y
        const cosRadians = Math.cos(radians)
        const sinRadians = Math.sin(radians)
        const x3 = cosRadians * dx - sinRadians * dy + center.x
        const y3 = sinRadians * dx + cosRadians * dy + center.y
        return { x: x3, y: y3 }
    }

    isInsidePolygon(point, points, angle = 0) {
        const centerPoint = this.getPolygonCenterPoint(points);
        const newPoints = points.map(p => this.rotatePoint(p, centerPoint, angle))
        const len = newPoints.length
        let cross = 0
        for (let i = 0; i < len; i++) {
            const p1 = newPoints[i]
            const p2 = newPoints[(i + 1) % len]
            if (p1.y === p2.y) continue
            if (point.y < Math.min(p1.y, p2.y)) continue
            if (point.y >= Math.max(p1.y, p2.y)) continue
            const x = ((point.y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x
            if (x > point.x) {
                cross++
            }
        }
        return cross % 2 === 1
    }
}